/*******************************************************************************
 * Copyright (c) 2000, 2025 IBM Corporation and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.gef.ui.palette;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.widgets.Display;

import org.eclipse.ui.IMemento;

import org.eclipse.draw2d.ColorProvider.SystemColorFactory;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.internal.InternalDraw2dUtils;

import org.eclipse.gef.EditDomain;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.editparts.SimpleRootEditPart;
import org.eclipse.gef.internal.SimpleAutoscaledRootEditPart;
import org.eclipse.gef.internal.ui.palette.PaletteSelectionTool;
import org.eclipse.gef.internal.ui.palette.editparts.DrawerEditPart;
import org.eclipse.gef.internal.ui.palette.editparts.PaletteStackEditPart;
import org.eclipse.gef.internal.ui.palette.editparts.ToolEntryEditPart;
import org.eclipse.gef.palette.PaletteDrawer;
import org.eclipse.gef.palette.PaletteEntry;
import org.eclipse.gef.palette.PaletteListener;
import org.eclipse.gef.palette.PaletteRoot;
import org.eclipse.gef.palette.PaletteStack;
import org.eclipse.gef.palette.ToolEntry;
import org.eclipse.gef.ui.palette.customize.PaletteCustomizerDialog;
import org.eclipse.gef.ui.palette.editparts.PaletteAnimator;
import org.eclipse.gef.ui.palette.editparts.PaletteEditPart;
import org.eclipse.gef.ui.parts.PaletteViewerKeyHandler;
import org.eclipse.gef.ui.parts.ScrollingGraphicalViewer;

/**
 * Graphical viewer for the GEF palette.
 *
 * @author Randy Hudson
 * @author Pratik Shah
 */
public class PaletteViewer extends ScrollingGraphicalViewer {

	private class PreferenceListener implements PropertyChangeListener {
		/**
		 * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
		 */
		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			String property = evt.getPropertyName();
			EditPart root = getRootEditPart().getContents();
			if (property.equals(PaletteViewerPreferences.PREFERENCE_FONT)) {
				updateFont();
				refreshAllEditParts(root);
			} else if (property.equals(PaletteViewerPreferences.PREFERENCE_LAYOUT)
					|| property.equals(PaletteViewerPreferences.PREFERENCE_AUTO_COLLAPSE)
					|| property.equals(DefaultPaletteViewerPreferences
							.convertLayoutToPreferenceName(getPaletteViewerPreferences().getLayoutSetting()))) {
				refreshAllEditParts(root);
			} else if (property.equals(PaletteViewerPreferences.PREFERENCE_SCROLLBARS_MODE)) {
				FigureCanvas canvas = getFigureCanvas();
				if (canvas != null) {
					canvas.setScrollbarsMode((int) evt.getNewValue());
				}
			}
		}

		private void refreshAllEditParts(EditPart part) {
			part.refresh();
			part.getChildren().forEach(this::refreshAllEditParts);
		}
	}

	private static final PaletteViewerPreferences PREFERENCE_STORE = new DefaultPaletteViewerPreferences();
	private ToolEntry activeEntry = null;
	private PaletteCustomizer customizer = null;
	private PaletteCustomizerDialog customizerDialog = null;
	private boolean globalScrollbar = false;
	private final List<PaletteListener> paletteListeners = new CopyOnWriteArrayList<>();
	private PaletteRoot paletteRoot = null;
	private final PreferenceListener prefListener = new PreferenceListener();
	private PaletteViewerPreferences prefs = PREFERENCE_STORE;
	private Font font = null;
	private PaletteAnimator paletteAnimator;
	private PaletteColorProvider colorProvider = PaletteColorProvider.INSTANCE;

	/**
	 * Constructor
	 */
	public PaletteViewer() {
		EditDomain domain = new EditDomain();
		domain.setDefaultTool(new PaletteSelectionTool());
		domain.loadDefaultTool();
		setEditDomain(domain);
		setKeyHandler(new PaletteViewerKeyHandler(this));
		setEditPartFactory(new PaletteEditPartFactory());
	}

	/**
	 * Adds the given PaletteListener as the one to be notified when the active tool
	 * on the palette changes.
	 *
	 * @param paletteListener The listener that needs to be notified of active tool
	 *                        changes on the palette
	 */
	public void addPaletteListener(PaletteListener paletteListener) {
		paletteListeners.add(paletteListener);
	}

	/**
	 * @see org.eclipse.gef.ui.parts.GraphicalViewerImpl#createDefaultRoot()
	 */
	@Override
	protected void createDefaultRoot() {
		setRootEditPart(InternalDraw2dUtils.isAutoScaleEnabled() ? new SimpleAutoscaledRootEditPart()
				: new SimpleRootEditPart());
	}

	private void disposeFont() {
		if (font != null) {
			font.dispose();
			font = null;
		}
	}

	/**
	 * Indicates that the palette should scroll using a native vertical scrollbar as
	 * opposed to individual lightweight buttons that appear dynamically on each
	 * drawer. The default settings is <code>false</code>. Enabling this setting
	 * requires additional horizontal screen space for the scrollbar. Therefore, its
	 * use is discouraged.
	 * <p>
	 * This setting must be changed prior to calling
	 * {@link ScrollingGraphicalViewer#createControl(org.eclipse.swt.widgets.Composite)}
	 * . After the control is created, changing this setting will have no effect.
	 * </p>
	 *
	 * @param value <code>true</code> if a vertical scrollbar should be displayed
	 */
	public void enableVerticalScrollbar(boolean value) {
		this.globalScrollbar = value;
	}

	/**
	 * @return the DrawerEditPart that has the given part; part, if part is a
	 *         drawer; null, if part is not in a drawer
	 */
	private DrawerEditPart findContainingDrawer(EditPart part) {
		if (part == null) {
			return null;
		}
		if (part instanceof DrawerEditPart dEP) {
			return dEP;
		}
		return findContainingDrawer(part.getParent());
	}

	/**
	 * Notifies registered listeners of change in the active tool on the palette
	 */
	protected void fireModeChanged() {
		paletteListeners.forEach(pl -> pl.activeToolChanged(this, activeEntry));
	}

	/**
	 * @return the customizer
	 */
	public PaletteCustomizer getCustomizer() {
		return customizer;
	}

	/**
	 * NOTE: A PaletteCustomizer must be set for this viewer using the
	 * {@link #setCustomizer(PaletteCustomizer)} method before this method is
	 * invoked.
	 *
	 * @return The dialog that can be used to customize entries on the palette
	 */
	public PaletteCustomizerDialog getCustomizerDialog() {
		if (customizerDialog == null) {
			customizerDialog = new PaletteCustomizerDialog(getControl().getShell(), getCustomizer(), getPaletteRoot());
		}
		return customizerDialog;
	}

	/**
	 * @return the entry for the currently active tool
	 */
	public ToolEntry getActiveTool() {
		return activeEntry;
	}

	/**
	 * Gets the PaletteAnimator for this palette
	 *
	 * @return the palletteAnimator to be used for palette animations
	 * @since 3.19
	 */
	public PaletteAnimator getPaletteAnimator() {
		return paletteAnimator;
	}

	/**
	 * Returns the palette's root model.
	 *
	 * @return the palette root
	 */
	public PaletteRoot getPaletteRoot() {
		return paletteRoot;
	}

	/**
	 * @return The PaletteViewerPreferences that this palette is using to store its
	 *         preferences (if none has been set, it returns the default one, which
	 *         uses the GEF preference store)
	 */
	public PaletteViewerPreferences getPaletteViewerPreferences() {
		return prefs;
	}

	/**
	 * @return The color provider that is used for painting the figures of this
	 *         viewer. If no palette has been set by the user,
	 *         {@link SystemColorFactory#getColorProvider()} is returned instead of
	 *         {@code null}.
	 * @since 3.20
	 */
	public PaletteColorProvider getColorProvider() {
		return colorProvider;
	}

	private ToolEntryEditPart getToolEntryEditPart(ToolEntry entry) {
		return (ToolEntryEditPart) getEditPartForModel(entry);
	}

	/**
	 * @see org.eclipse.gef.ui.parts.GraphicalViewerImpl#handleDispose(org.eclipse.swt.events.DisposeEvent)
	 */
	@Override
	protected void handleDispose(DisposeEvent e) {
		super.handleDispose(e);
		disposeFont();
	}

	/**
	 * @see org.eclipse.gef.ui.parts.GraphicalViewerImpl#handleFocusGained(FocusEvent)
	 */
	@Override
	protected void handleFocusGained(FocusEvent fe) {
		super.handleFocusGained(fe);
		/*
		 * Fix for Bug# 63297 When the palette gets focus, the LWS will take care of
		 * setting focus on the correct figure. However, when the user clicks on an
		 * entry, the focus is "thrown away." In that case, the LWS would set focus on
		 * the first focusable figure. We override that here and set focus on the
		 * correct/selected figure. Invoking getFocusEditPart().setFocus(true) does not
		 * work because that editpart thinks it already has focus (it's not aware of
		 * focus being thrown away) and does nothing.
		 */
		if (focusPart == null) {
			IFigure fig = ((GraphicalEditPart) getFocusEditPart()).getFigure();
			fig.internalGetEventDispatcher().requestFocus(fig);
		}
	}

	/**
	 * @see org.eclipse.gef.ui.parts.GraphicalViewerImpl#hookControl()
	 */
	@Override
	protected void hookControl() {
		super.hookControl();
		FigureCanvas canvas = getFigureCanvas();
		canvas.getViewport().setContentsTracksWidth(true);
		canvas.getViewport().setContentsTracksHeight(!globalScrollbar);
		canvas.setHorizontalScrollBarVisibility(FigureCanvas.NEVER);
		canvas.setVerticalScrollBarVisibility(globalScrollbar ? FigureCanvas.ALWAYS : FigureCanvas.AUTOMATIC);
		if (prefs != null) {
			canvas.setScrollbarsMode(prefs.getScrollbarsMode());
			prefs.addPropertyChangeListener(prefListener);
		}
		updateFont();
	}

	/**
	 * Returns true if the given PaletteDrawer is expanded
	 *
	 * @param drawer the PaletteDrawer
	 * @return true if expanded
	 */
	public boolean isExpanded(PaletteDrawer drawer) {
		EditPart ep = getEditPartForModel(drawer);
		if (ep instanceof DrawerEditPart dep) {
			return dep.isExpanded();
		}
		return false;
	}

	/**
	 * Returns true if the given PaletteDrawer is pinned
	 *
	 * @param drawer the PaletteDrawer
	 * @return true if pinned
	 */
	public boolean isPinned(PaletteDrawer drawer) {
		EditPart ep = getEditPartForModel(drawer);
		if (ep instanceof DrawerEditPart dep) {
			return dep.isPinnedOpen();
		}
		return false;
	}

	/**
	 * The given PaletteListener will not be notified of active tool changes in the
	 * palette.
	 *
	 * @param paletteListener the PaletteListener which doesn't want to be notified
	 *                        of active tool changes in the palette anymore
	 */
	public void removePaletteListener(PaletteListener paletteListener) {
		paletteListeners.remove(paletteListener);
	}

	/**
	 * Tries to apply the state of the given IMemento to the contents of this
	 * viewer. It fails silently, i.e. no exceptions are thrown if the given state
	 * could not be applied.
	 *
	 * @param memento The memento that has the state to be applied to the contents
	 *                of this viewer
	 * @return a boolean indicating whether or not the given memento was
	 *         successfully applied
	 * @since 3.0
	 */
	public boolean restoreState(IMemento memento) {
		try {
			PaletteEditPart part = (PaletteEditPart) getEditPartForModel(getPaletteRoot());
			if (part != null) {
				part.restoreState(memento);
			}
		} catch (RuntimeException re) {
			/*
			 * @TODO:Pratik Perhaps you should log this exception. Or not catch it at all.
			 */
			return false;
		}
		return true;
	}

	/**
	 * @see ScrollingGraphicalViewer#reveal(EditPart)
	 */
	@Override
	public void reveal(EditPart part) {
		// If the given part is a drawer, we don't need to expand it. Hence,
		// when invoking
		// findContainingDrawer(), we use part.getParent()
		DrawerEditPart drawer = findContainingDrawer(part.getParent());
		if (drawer != null && !drawer.isExpanded()) {
			drawer.setExpanded(true);
		}
		// if the part is inside a stack, set it to be the top level item of the
		// stack.
		if (part.getParent() instanceof PaletteStackEditPart) {
			((PaletteStack) part.getParent().getModel()).setActiveEntry((PaletteEntry) part.getModel());
		}
		super.reveal(part);
	}

	/**
	 * Captures the state of the contents of this viewer in the given memento
	 *
	 * @param memento the IMemento in which the state is to be saved
	 * @since 3.0
	 */
	public void saveState(IMemento memento) {
		// Bug# 69026 - The PaletteRoot can be null initially for VEP
		PaletteEditPart base = (PaletteEditPart) getEditPartForModel(getPaletteRoot());
		if (base != null) {
			base.saveState(memento);
		}
	}

	/**
	 * Sets the customizer.
	 *
	 * @param customizer the customizer to be set
	 */
	public void setCustomizer(PaletteCustomizer customizer) {
		this.customizer = customizer;
	}

	/**
	 * Sets the active entry for this palette. The Editpart for the given entry will
	 * be activated (selected).
	 *
	 * @param newMode the ToolEntry whose EditPart has to be set as the active tool
	 *                in this palette
	 */
	public void setActiveTool(ToolEntry newMode) {
		if (newMode == null) {
			newMode = getPaletteRoot().getDefaultEntry();
		}
		if (activeEntry != null) {
			getToolEntryEditPart(activeEntry).setToolSelected(false);
		}
		activeEntry = newMode;
		if (activeEntry != null) {
			ToolEntryEditPart editpart = getToolEntryEditPart(activeEntry);
			if (editpart != null) {
				editpart.setToolSelected(true);
			}
		}
		fireModeChanged();
	}

	/**
	 * Sets the PaletteAnimator for this palette
	 *
	 * @param paletteAnimator the PaletteAnimator to be used for this
	 *                        palletteViewer's children
	 * @since 3.19
	 */
	public void setPaletteAnimator(PaletteAnimator paletteAnimator) {
		this.paletteAnimator = paletteAnimator;
	}

	/**
	 * Sets the root for this palette.
	 *
	 * @param root the PaletteRoot for this palette
	 */
	public void setPaletteRoot(PaletteRoot root) {
		if (root == paletteRoot) {
			return;
		}
		paletteRoot = root;
		if (paletteRoot != null) {
			EditPart palette = getEditPartFactory().createEditPart(getRootEditPart(), root);
			getRootEditPart().setContents(palette);
		}
	}

	/**
	 * This palette will use the given PaletteViewerPreferences to store all its
	 * preferences.
	 * <p>
	 * NOTE: This method should be invoked by a client only once (before the first
	 * time {@link #getPaletteViewerPreferences()} is invoked). Trying to invoke
	 * this method after that could lead to problems where some preferences would
	 * still be stored in the old preference store.
	 * </p>
	 *
	 * @param prefs the PaletteViewerPreferences that is to be used to store all the
	 *              preferences for this palette
	 */
	public void setPaletteViewerPreferences(PaletteViewerPreferences prefs) {
		if (this.prefs != null) {
			this.prefs.removePropertyChangeListener(prefListener);
		}
		this.prefs = prefs;
		if (getControl() != null && !getControl().isDisposed()) {
			this.prefs.addPropertyChangeListener(prefListener);
		}
	}

	/**
	 * Sets the color provider that is used for painting the figures of this viewer.
	 * If {@code null} is passed as an argument, the palette is reset to its default
	 * configuration.
	 *
	 * @param colorProvider The color provider for this viewer.
	 * @since 3.20
	 */
	public void setColorProvider(PaletteColorProvider colorProvider) {
		if (colorProvider == null) {
			this.colorProvider = PaletteColorProvider.INSTANCE;
		} else {
			this.colorProvider = colorProvider;
		}
	}

	/**
	 * @see org.eclipse.gef.ui.parts.AbstractEditPartViewer#unhookControl()
	 */
	@Override
	protected void unhookControl() {
		super.unhookControl();
		disposeFont();
		if (prefs != null) {
			prefs.removePropertyChangeListener(prefListener);
		}
	}

	private void updateFont() {
		disposeFont();

		if (getControl() == null || getControl().isDisposed()) {
			return;
		}

		font = new Font(Display.getCurrent(), getPaletteViewerPreferences().getFontData());
		getControl().setFont(font);
		getFigureCanvas().getViewport().invalidateTree();
		getFigureCanvas().getViewport().revalidate();
		getFigureCanvas().redraw();
	}

}
